import java.net.*;
import java.io.*;
import java.util.*;

/**
 * The ClientHandler class is responsible for handling a single client via a dedicated socket it
 * recieves a clients input and sends this to all handlers. this message is then recieved by
 * all clients via their own ClientHandler (including the broadcasting one) allowing subsequent display of
 * the message.
 *
 * @author BluJaded Team
 * @version (see Sourceforge.net)
 */
public class ClientHandler extends Thread {
  
  // the maximum number of clients that can connect to the server at any one time.
  private static final int MAXCLIENTS = 3;
  
  // the server associated to this ClientHandler
  private static Server server;
  
  // Message variable to hold incoming messages
  private Message msg;
  
  // nickname associated to the client of this ClientHandler
  private String nickname;
  
  // client nickname that this client is whispering to
  private String wispTo ="";

  // Socket associated to this handler and client pair
  private Socket socket;
   
  // InputStream of the Socket associated to this handler and client pair
  private ObjectInputStream input;
  
  // OutputStream of the Socket associated to this handler and client pair
  private ObjectOutputStream output;
    
  // switch variable used to terminate the thread cleanly
  private boolean run = true;   
  
  // IP address of the client
  private String clientIP;
  
  // used by the checknickname method to return a nickname match when found
  private static String nickMatch;

    /**
     * Creates a static vector (similar to an ArrayList) to hold a list of ClientHandler objects.
     */
    public static Vector <ClientHandler> handlers = new Vector<ClientHandler>();
  
  /**
   * Creates a ClientHandler object associated to the Socket provided and establishes
   * input and output streams for that socket.
   *
   * @param clientSocket    the socket associated to a connected client.
   * @param myServer        Server this ClientHandler is associated with
   *
   * @throws IOException if stream initialisation fails an IOException is thrown
   */
  public ClientHandler(Socket clientSocket, Server myServer) throws IOException{
    socket = clientSocket;
    // retrieves the IP address of the client in String format
    clientIP = socket.getInetAddress ().toString ();
    server = myServer;
    
    // retrieves the OutputStream of the Socket
    OutputStream os = socket.getOutputStream();
    // associates the OutputStream with an ObjectOutputStream Object allowing transfer of serialized objects
    output = new ObjectOutputStream(os);
    // flushes the ObjectOutputStream sending the header required by the paired ObjectInputStream of the Client
    output.flush();
    
    // retrieves the InputStream of the Socket
    InputStream is = socket.getInputStream();
    // associates the InputStream with an ObjectInputStream Object allowing transfer of serialized objects
    input = new ObjectInputStream(is);
    
  }

  /**
   * Run method required by the superclass Thread to override the run method required by the implementation of Runnable
   * is executed when a new thread, once created, is started.
   * Sets up an infinate loop that distributes upon reciept, any message it recieves from the client until terminated.
   * If the thread terminates then appropriate clean up occurs.
  **/
  public void run() {
      try {
          // reads a serialized Message object from the ObjectInputStream
          msg = (Message) input.readObject();
          // sets the local variable nickname to the nickname specified in the message
          nickname = msg.getNick();
          
          // while the nickname in the local variable is invalid
          while (!checkNickname(nickname)){
              // create new Message object
              Message sndMsg = new Message();
              // set the messages content
              sndMsg.setContent("** Your Nickname is taken on this Server. **\n" +
                      "** Please choose another in the format /nick <nickname> **");
              // write the serrialized Message object directly to the ObjectOutputStream as method
              // distribute() to this single Client is not possible yet without a valid nickname
              output.writeObject( sndMsg );
              // flush the ObjectOutputStream sending the Message object to the Client
              output.flush();
              // reads a serialized Message object from the ObjectInputStream
              msg = (Message) input.readObject();
              // if the user closed the window
              if (msg.getQuit()){
                  return;
              }
              else{
                // sets the local variable nickname to the nickname specified in the message
                if (!msg.getNick().equals("")){
                    nickname=msg.getNick();
                }
              }
          }
          
          // adds this handler to the collection of connected ClientHandlers
          handlers.addElement(this);
          // creates a new Message object
          Message sndMsg = new Message();
          // set the messages content
          sndMsg.setContent("**You are logged in as : " + nickname +"**");
          // specifies the to of the message to be this ClientHandlers associated Client
          sndMsg.setMsgTo(nickname);
          // specifies that it is only that client that the message is to be sent to
          sndMsg.setOnly(true);
          // sends the message to the specified Client only
          distribute(sndMsg);
      } 
      catch (IOException ex) {
          // if an error occurs prints the stack trace to assist debugging
          // ex.printStackTrace();
      } 
      catch (ClassNotFoundException ex) {
          // if an error occurs prints the stack trace to assist debugging
          // ex.printStackTrace();
      }
//      finally{
  //        if (msg.getQuit()){
               //   return;
    //      }
     // }
      
      
      try {
          // if there are more clients than the MAXCLIENTS constant currently connected to the server
          if (handlers.size()>MAXCLIENTS){
              // create a new Message object
              Message sndMsg = new Message();
              // set the messages content
              sndMsg.setContent("** The Server is Full Connection Refused **");
              // specifies the to of the message to be this ClientHandlers associated Client
              sndMsg.setMsgTo(nickname);
              // specifies that it is only that client that the message is to be sent to
              sndMsg.setOnly(true);
              // sends the message to the Client specified only
              distribute(sndMsg);
              
              Server.writeToLog("New Client conection refused from: " + clientIP);
   
              // exits the run method thereby invoking the finally section bellow
              return;   
          }
          
          Server.writeToLog("New Client at "+ clientIP + " logs in as: " + nickname);
          
          // creates a new message
          Message newmsg = new Message();
          // set the messages content
          newmsg.setContent(nickname + " has joined the discussion.");
          // sets the nickname variable of the message to the chosen nickname
          newmsg.setNick(nickname);
          // specifies the to of the message to be this ClientHandlers associated Client
          newmsg.setMsgTo(nickname);
          // sends the Message to all Clients except the one specified in to
          distribute(newmsg);
          
          // begins a loop that is only left when an exception occurs or when the run variable is set to false
          while(run) {
              try{
                  // reads a serialized Message object from the ObjectInputStream
                  msg = (Message) input.readObject();
                  // if the messages nickname variable is not equal to the locally stored nickname variable
                  if(!msg.getNick().equals(nickname)){
                      // if the nickname variable of the message is a valid nickname
                      if(checkNickname(msg.getNick())){
                          // stores the current value of the local nickname variable in a temporary variable
                          String oldNick= nickname;
                          // stores the messages nickname variable in the local nickname variable
                          nickname=msg.getNick();
                          // creates a new message
                          Message sndMsg = new Message();
                          // set the messages content
                          sndMsg.setContent("**" + oldNick + " changes Nickname to: " + nickname + "**");
                          // sends the message to all Clients
                          distribute(sndMsg);
                          // update the wipsTo's of any clients whispering to this client
                          changeWispTo(oldNick,nickname);
                          
                          Server.writeToLog(oldNick + " changes nickname to: " + nickname);
                          
                      } 
                      else{
                          //if the nickname was invalid
                          //creates a new message
                          Message sndMsg = new Message();
                          //set the messages content
                          sndMsg.setContent("**Unable to change Nickname. "+msg.getNick()+" is taken on this Server**");
                          //specifies the to of the message to be this ClientHandlers associated Client
                          sndMsg.setMsgTo(nickname);
                          //sprecifies that it is only that client that the message is to be sent to
                          sndMsg.setOnly(true);
                          //sends the message to the Client specified only
                          distribute(sndMsg);
                      }
                  } 
                  else{
                      
                      //if the msgTo variable of the message is not empty, ie the message is a whisper command
                      if (!msg.getMsgTo().equals("")){
                         
                          //check that the nickname provided in msgTo is a valid nickname
                         if (!checkNickname(msg.getMsgTo())){
                             
                             //set the wispTo variable to the nickname provided, thereby entering whisper mode
                             wispTo=nickMatch;
                             
                             //if the wisp value of the message has not been flagged
                             //sends the conformation message to the client.
                             if (!msg.getWisp()){
                                 //creates a new message
                                 Message sndMsg = new Message();
                                 //set the messages content
                                 sndMsg.setContent("**whisper Mode - You are whispering to: "+wispTo+"**");
                                 //specifies that the attempt to enter whisper mode succeeded
                                 sndMsg.setWispSuccess(true);
                                 //used to send the matched nickname back to the Client incase it differs due to case
                                 sndMsg.setMsgFrom(wispTo);
                                 //specifies the to of the message to be this ClientHandlers associated Client
                                 sndMsg.setMsgTo(nickname);
                                 //sprecifies that it is only that client that the message is to be sent to
                                 sndMsg.setOnly(true);
                                 //sends the message to the Client specified only
                                 distribute(sndMsg);
                             }
                             else{
                                 //creates a new message
                                 Message sndMsg = new Message();
                                 //specifies that the attempt to enter whisper mode succeeded
                                 sndMsg.setWispSuccess(true);
                                 //msgFrom used in this instance to mark this message as a whisper attempt result (a success as noted by the flag above)
                                 sndMsg.setMsgFrom(nickname);
                                 //specifies the to of the message to be this ClientHandlers associated Client
                                 sndMsg.setMsgTo(nickname);
                                 //sprecifies that it is only that client that the message is to be sent to
                                 sndMsg.setOnly(true);
                                 //sends the message to the Client specified only
                                 distribute(sndMsg);
                             }
                             
                             Server.writeToLog(nickname + " enters whisper Mode to: " + wispTo);
                         }
                         else{
                             Message sndMsg = new Message();
                             //set the messages content
                             sndMsg.setContent("**Unable to whisper to "+msg.getMsgTo()+" that Nickname is not recognised**");
                             //specifies the to of the message to be this ClientHandlers associated Client
                             sndMsg.setMsgTo(nickname);
                             //msgFrom used in this instance to mark this message as a whisper attempt result (a fail in this case)
                             sndMsg.setMsgFrom(nickname);
                             //sprecifies that it is only that client that the message is to be sent to
                             sndMsg.setOnly(true);
                             //sends the message to the Client specified only
                             distribute(sndMsg);
                         }
                            
                      }
                      else{
                          //if the Message is a quit command
                          if (msg.getQuit()){
                              //create a new message
                              Message leftmsg = new Message();
                              //set the messages content
                              leftmsg.setContent(nickname + " has left the discussion.");
                              //distribute the Message to all clients
                              distribute(leftmsg);
                          } 
                          else{
                              //if the Message is a server quit command
                              if (msg.getServerQuit()) {
                                  //create a new message
                                  Message quitmsg = new Message();
                                  //set the messages content
                                  quitmsg.setContent("The Server has been terminated by " + nickname);
                                  //distribute the Message to all clients
                                  distribute(quitmsg);
                                  //changes the switch variable "run" to terminate the Server smoothly
                                  Server.terminateServer();
                              } 
                              else{
                                  //if the Message is a list command
                                  if (msg.getListClients()){
                                      //creates a new message
                                      Message listmsg = new Message();
                                      //set the messages content using the makelist() method
                                      listmsg.setContent("************************************************************************\n" +
                                              "The Following Clients are connected to this server:\n"+ makeList() +
                                              "************************************************************************");
                                      //specifies the to of the message to be this ClientHandlers associated Client
                                      listmsg.setMsgTo(nickname);
                                      //sprecifies that it is only that client that the message is to be sent to
                                      listmsg.setOnly(true);
                                      //sends the message to the Client specified only
                                      distribute(listmsg);
                                  } 
                                  else {
                                      if (msg.getEndWisp()){
                                          wispTo="";
                                          if (!msg.getWisp()){
                                              Message sndMsg = new Message();
                                              //set the messages content
                                              sndMsg.setContent("**Broadcast Mode - You are now Broadcasting to all Clients**");
                                              //specifies the to of the message to be this ClientHandlers associated Client
                                              sndMsg.setMsgTo(nickname);
                                              //sprecifies that it is only that client that the message is to be sent to
                                              sndMsg.setOnly(true);
                                              //sends the message to the Client specified only
                                              distribute(sndMsg);
                                          }
                                          Server.writeToLog(nickname + " Leaves whisper Mode and reenters Broadcast Mode" );
                                      } 
                                      else{
                                          if (msg.getHelp()){
                                            helpMsg();
                                          }
                                          else {
                                              //if its a normal Message
                                              //create a new message
                                              Message fwdmsg = new Message();
                                              //set the messages content
                                              fwdmsg.setContent(nickname + " - " + msg.getContent());
                                              //if the client is whispering to someone
                                              if (!wispTo.equals("")){
                                                  //sets the content of the message to just the messag to be sent (no senders nickname)
                                                  fwdmsg.setContent(msg.getContent());
                                                  //specifies the to of the message to be the nickname that this client is whispering to
                                                  fwdmsg.setMsgTo(wispTo);
                                                  //specifies the from of the message to be this clients nickname
                                                  fwdmsg.setMsgFrom(nickname);
                                                  //specifies that the provided nickname is the only client that the message is to be sent to
                                                  fwdmsg.setOnly(true);
                                                  //marks the message as a whisper
                                                  fwdmsg.setWisp(true);
                                                  
                                                  //sends a message back to just just the Client with what they whispered
                                                  //creates a new message object
                                                  Message sndMsg = new Message();
                                                  //set the messages content
                                                  sndMsg.setContent("You whisper to: " + wispTo + " - " + msg.getContent());
                                                  //specifies the to of the message to be this ClientHandlers associated Client
                                                  sndMsg.setMsgTo(nickname);
                                                  //specifies that it is only that client that the message is to be sent to
                                                  sndMsg.setOnly(true);
                                                  //sends the message to the Client specified only
                                                  distribute(sndMsg);
                                              }
                                              //distribute the Message to all clients
                                              distribute(fwdmsg);
                                          }
                                      }
                                  }
                              }
                            }
                        }
                  }
              }
              //catches the exception should the object read not be a Message object
              catch (ClassNotFoundException e){}
              
          }
      }
      
      catch (IOException ex) {
          //if an error occurs prints the stack trace to assist debugging
          //ex.printStackTrace ();
      }
      
      //the following code runs once when the run method, and therefore the thread, terminates
      finally {
          
          //removes the handler from the collection of connected ClientHandlers
          handlers.removeElement(this);
          
          //if the number of connected clients is within the limit, ie the leaving person was not refused connection
          if (handlers.size()<=MAXCLIENTS){  
            Server.writeToLog("Client "+ nickname +" at " + clientIP + " has disconnected from the Server");
          }
          
          //updates the wispTo of any Clients whispering to this person to "" thereby reentering them to Broadcast Mode
          changeWispTo(nickname, "");
          
          //updates the client list on all clients gui's
          //create a new message
          Message updateMsg = new Message();
          //sets the Connected variable of the Message to a list of the connected clients using the makeList() method
          updateMsg.setConnected(makeList());
          //distribute the Message to all clients
          distribute(updateMsg);
                              
          try {
              //closes the socket connecting this client/handler pair
              socket.close();
          }
          
          catch (IOException ex) {
              //if an error occurs prints the stack trace to assist debugging
              //ex.printStackTrace();
          }
          
      }
      
  }
   /**
    * Updates the wispTo of any Clients whispering from the old nickname to the new nickname.
    * Used to maintain whisper mode when the target changes their nickname or to re-enter
    * them to Broadcast Mode if the target leaves the server.
    *
    * @param oldNick the nickname to change
    * @param newNick the nickname to change the old nickname to 
    */
   private void changeWispTo(String oldNick, String newNick){
       
   // synchronized access to the handlers collection preventing concurrent access whilst in use
   synchronized (handlers) {
      
      // iterates over the handlers collection
       for(ClientHandler handler :  handlers) {

        // if ClientHandler in question's wispTo variable is equal to the provided oldNick
        if (handler.wispTo.equals(oldNick)){

            // change the ClientHandler in question's wispTo variable to the provided newNick
            handler.wispTo = newNick;

            // if the client is being returned to whisper mode
            if (newNick.equals("")){
                // create a new message object
                Message sndMsg = new Message();
                // set the messages content
                sndMsg.setContent("** " + oldNick + " has disconnected from the server **" +
                        "\n** You have been returned to Broadcast Mode **");
                // specifies the to of the message to be this ClientHandlers associated Client
                sndMsg.setMsgFrom(handler.nickname);
                sndMsg.setMsgTo(handler.nickname);
                // specifies that it is only that client that the message is to be sent to
                sndMsg.setOnly(true);
                // sends the message to the Client specified only
                distribute(sndMsg);
                
                Server.writeToLog(handler.nickname + " Leaves whisper Mode and returns to Broadcast Mode" );
            }
            else{
                // if nickname has just being changed to another nickname
                // create a new message object
                Message sndMsg = new Message();
                // set the messages content
                sndMsg.setContent("** " + oldNick + " has changed their Nickname to: " + newNick + " **" +
                        "\n** You are now whispering to: " + newNick + " **");
                // specifies the to of the message to be this ClientHandlers associated Client
                sndMsg.setMsgTo(handler.nickname);
                // specifies that it is only that client that the message is to be sent to
                sndMsg.setOnly(true);
                // sends the message to the Client specified only
                distribute(sndMsg);
                
                Server.writeToLog(handler.nickname + " enters whisper Mode to: " + newNick);
            }
        }
      }
    }    
  }
  
  /**
   * Creates a list of the currently connected clients nicknames.
   *
   * @return the string list of nicknames as strings seperated by new lines
   */
  private static String makeList(){
      
   String clientList="";
   
   synchronized (handlers) {
      
      // creates an iterator for the handlers collection
       for (ClientHandler handler : handlers) {
           // iterates through the collection..
           // adds the Clients nickname to the list
           clientList += " " + handler.nickname + "\n";
       }
    }
    //returns the created list
    return clientList;      
  }

    /**
     * Checks that the nickname passed to it is not in use by any client on the server or that the nickname exists
     * depending on use.
     *
     * @param nick    the nickname to check
     * @return returns true if nickname does not currently exist else returns false
     */
    private static boolean checkNickname(String nick){
        //synchronized access to the handlers collection preventing concurrent access whilst in use
        synchronized (handlers) {

            //creates an iterator for the handlers collection
            for (ClientHandler handler : handlers) {
                //iterates through the collection
                //if the provided nickname is in use
                if (handler.nickname.toLowerCase().equals(nick.toLowerCase())) {
                    nickMatch = handler.nickname;
                    return false;

                }
            }

        }
        nickMatch = "";
        //if the method has not already returned false then
        return true;
    }

    /**
     *  Terminates the ClientHandler cleanly using the switch variable run.
     */
    public void terminateHandler(){
        run = false;
    }

    /**
     * Synchronized sending of the message object by the ClientHandler.
     *
     * @param handler   ClientHandler used to send message
     * @param msg       Message to be sent
     */
    private static void synchroSendMsg(ClientHandler handler, Message msg){
        try{
            // synchronized access to the handlers outputstream preventing concurrent access whilst in use
            synchronized (handler.output) {
                // writes the message to the output stream of the handler in question
                handler.output.writeObject(msg);
            }
            // flushes the stream, sending the message
            handler.output.flush();
        }
        catch (IOException ex) {
            //if an error occurs terminates the thread cleanly by terminating the loop using the switch variable run
            handler.terminateHandler();
        }

    }

    /**
     * Distributes a message to all connected clients by iterating through the handlers collection
     * and sending the message to the ObjectOutputStream.
     *
     * @param  message    the Message object to be sent to all clients
     */
    private static void distribute(Message message) {
        // synchronized access to the handlers collection preventing concurrent access whilst in use
        synchronized (handlers) {
            // creates an iterator for the handlers collection
            for (ClientHandler handler : handlers) {
                // iterates through the collection
                // sets the messages nickname variable to the nickname stored locally by the handler in question
                message.setNick(handler.nickname);
                // sets the Connected variable of the Message to a list of the connected clients using the makeList()
                // method
                message.setConnected(makeList());
                // if no 'to' variable has been supplied
                if (message.getMsgTo().equals("")) {
                    // Send synchronized Message
                    synchroSendMsg(handler, message);
                } else {
                    // if a 'to' variable has been supplied and the 'only' variable has been set to true
                    if (message.getOnly()) {
                        // if the handler in questions nickname is the same as the 'to' variable of the message
                        if (handler.nickname.equals(message.getMsgTo())) {
                            // Send synchronized Message
                            synchroSendMsg(handler, message);
                            //returns therefore exiting the iteration and in turn, method
                            return;
                        }
                    } else {
                        //if a to variable has been supplied and the only variable is still false
                        //if the handler in questions nickname is not the same as the to variable of the message
                        if (!handler.nickname.equals(message.getMsgTo())) {
                            // Send synchronized Message
                            synchroSendMsg(handler, message);
                        }
                    }
                }



            }
        }
    }


    /**
     *  Sends the client a list of commands.
     */
    private void helpMsg(){
        // creates a new message object
        Message sndMsg = new Message();
        // set the messages content
        sndMsg.setContent("************************************************************************\n" +
                          "The following Commands are recognised by JadeTalk:\n" +
                          "\n" +
                          " /list               -  Displays a list of the currenly connected clients.\n" +
                          " /b                  -  Returns you to Broadcast Mode.\n" +
                          " /quit               -  Disconnects you and closes the Client window.\n" +
                          " /quitserver         -  Terminates the server, all clients will be informed\n" +
                          "                             and then disconnected.\n" +
                          " /nick <nickname>    -  Will change your nickname to the nickname\n"+
                          "                             provided if possible.\n" +
                          " /w <nickname>       -  Will enter whisper Mode to the Client with the\n" +
                          "                             nickname provided if possible.\n" +
                          " /w <nickname> <message>  -  Will whisper just the provided\n" +
                          "                                    message to the Client with the nickname\n" +
                          "                                    provided if possible, and then return you\n" +
                          "                                    to Broadcast Mode.\n" +
                          "************************************************************************\n");
        // specifies the 'to' attribute of the message to be this ClientHandlers associated Client
        sndMsg.setMsgTo(nickname);
        // specifies that it is only that client that the message is to be sent to
        sndMsg.setOnly(true);
        // sends the message to the Client specified only
        distribute(sndMsg);
    }
}